package com.tools.web.interceptor.auth._common;

import com.tools.common.object.Note;
import com.tools.common.object.json.JsonKit;
import com.tools.common.object.string.StrKit;
import com.tools.common.thread.OwnThreadFactory;
import com.tools.common.thread.Timeunit;
import com.tools.db.cache.local.regular.RegularEntry;
import com.tools.web.interceptor.auth.*;
import org.slf4j.Logger;

import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * 使用自定义本地缓存实现的 “用户多在线记录” 的 AuthenticationManager 身份认证拦截器
 * */
@Note("使用自定义本地缓存实现的 “用户多在线记录” 的 AuthenticationManager 身份认证拦截器")
public abstract class LocalMultipleAuthenticationManager extends GenericAuthenticationManager {

    @Note("缓存容器")
    private final ConcurrentHashMap<String, RegularEntry> cache;

    @Note("定时轮询的线程池，用于清理已过期的条目")
    private final ScheduledThreadPoolExecutor timePollingTor;

    @Note("日志输出的字符串边框")
    public static final String BORDER = StrKit.addString("=", 100);

    /* **************************************************************************************
     *
     *          构造器
     *
     * **************************************************************************************
     * */

    protected LocalMultipleAuthenticationManager(long execTime, TimeUnit execTimeUnit, AuthSource source, AuthUserService authUserService) {
        super(source, authUserService);
        super.protected_checkLoginDeviceTypeAndMaxOnlineTotalMap();
        this.cache = new ConcurrentHashMap<>();
        OwnThreadFactory schedulerFactory = new OwnThreadFactory("multipleAuthUser-cache-clean-", true);
        this.timePollingTor = new ScheduledThreadPoolExecutor(1, schedulerFactory);
        //开启定时轮询，清除不使用且已经过期的 key-value
        this.timePollingTor.scheduleAtFixedRate(this::private_timedPolling,
                execTime,
                execTime,
                execTimeUnit
        );
    }

    /* **************************************************************************************
     *
     *          实现和待子类实现的方法
     *
     * **************************************************************************************
     * */

    @Note("获取子类的实例名称")
    protected abstract String protected_getInstanceName();


    @Override
    public Timeunit getTimeunitUsedBySubclass() {
        return Timeunit.MILL;
    }

    @Override
    protected GetAuthUserResult getAuthUser(String key, AuthToken decryptedAuthToken) {
        RegularEntry entry = this.cache.get(key);
        if(entry == null) return super.protected_getResultIfExpired(decryptedAuthToken);
        entry.getLock().lock();
        try {
            MultipleAuthUser authUser = JsonKit.toEntity((String) entry.getValue(), MultipleAuthUser.class);
            ConcurrentHashMap<String, LinkedHashMap<String, Long>> keyAndDeviceAndLoginRecordMap =
                    authUser.getKeyAndDeviceAndLoginRecordMap();
            if(keyAndDeviceAndLoginRecordMap == null || keyAndDeviceAndLoginRecordMap.isEmpty()) {
                this.cache.remove(key);
                return super.protected_getResultIfExpired(decryptedAuthToken);
            }
            String loginDeviceType = decryptedAuthToken.getLoginDeviceType();
            LinkedHashMap<String, Long> loginRecordMap = keyAndDeviceAndLoginRecordMap.get(loginDeviceType);
            if(loginRecordMap == null || loginRecordMap.isEmpty()) {
                keyAndDeviceAndLoginRecordMap.remove(loginDeviceType);
                if(keyAndDeviceAndLoginRecordMap.isEmpty()) {
                    this.cache.remove(key);
                }
                return super.protected_getResultIfExpired(decryptedAuthToken);
            }
            String uuid = decryptedAuthToken.getUuid();
            Long expiration = loginRecordMap.get(uuid);
            if(expiration == null || expiration <= System.currentTimeMillis()) {
                loginRecordMap.remove(uuid);
                if(loginRecordMap.isEmpty()) {
                    keyAndDeviceAndLoginRecordMap.remove(loginDeviceType);
                    if(keyAndDeviceAndLoginRecordMap.isEmpty()) {
                        this.cache.remove(key);
                    }
                }
                return super.protected_getResultIfExpired(decryptedAuthToken);
            }
            authUser.setUuid(uuid);
            authUser.setLoginDeviceType(loginDeviceType);
            authUser.setLoginUserType(decryptedAuthToken.getLoginUserType());
            return new GetAuthUserResult(true, authUser);
        } finally {
            entry.getLock().unlock();
        }
    }

    @Override
    protected void setAuthUser(String key, AuthUser srcAuthUser, AuthToken decryptedAuthToken) {
        this.cache.compute(key, (k, entry) -> {
            String loginDeviceType = decryptedAuthToken.getLoginDeviceType();
            Map<String, Integer> loginDeviceTypeAndMaxOnlineTotal = super.getLoginDeviceTypeAndMaxOnlineTotal();
            int maxSize = (loginDeviceTypeAndMaxOnlineTotal != null && !loginDeviceTypeAndMaxOnlineTotal.isEmpty())
                    ? loginDeviceTypeAndMaxOnlineTotal.get(loginDeviceType)
                    : 0;
            if(maxSize < 1) return null;
            MultipleAuthUser newAuthUser = (MultipleAuthUser) srcAuthUser;
            //添加第一台登录设备
            if(entry == null) {
                ConcurrentHashMap<String, LinkedHashMap<String, Long>> keyAndDeviceAndLoginRecordMap =
                        newAuthUser.keyAndDeviceAndLoginRecordMapOrDefault();
                LinkedHashMap<String, Long> loginRecordMap = new LinkedHashMap<>(2);
                long expirationMill = decryptedAuthToken.getExpirationMill();
                loginRecordMap.put(decryptedAuthToken.getUuid(), expirationMill);
                keyAndDeviceAndLoginRecordMap.put(loginDeviceType, loginRecordMap);
                String json = JsonKit.toJSON(newAuthUser);
                return new RegularEntry(json, expirationMill, expirationMill);
            }
            //已有了
            entry.getLock().lock();
            try {
                MultipleAuthUser oldAuthUser = JsonKit.toEntity((String) entry.getValue(), MultipleAuthUser.class);
                ConcurrentHashMap<String, LinkedHashMap<String, Long>> keyAndDeviceAndLoginRecordMap =
                        oldAuthUser.keyAndDeviceAndLoginRecordMapOrDefault();
                LinkedHashMap<String, Long> loginRecordMap = keyAndDeviceAndLoginRecordMap.
                        computeIfAbsent(loginDeviceType, k1 -> new LinkedHashMap<>(2));
                String uuid = decryptedAuthToken.getUuid();
                long expirationMill = decryptedAuthToken.getExpirationMill();
                newAuthUser.copyAuthData(oldAuthUser);
                newAuthUser.copyCustomData(oldAuthUser);
                Object user = newAuthUser.getUser();
                oldAuthUser.setUser(user);
                //已经到了最大限制数量，强迫最先登录的那个下线
                if(loginRecordMap.size() >= maxSize) {
                    Iterator<String> iterator = loginRecordMap.keySet().iterator();
                    iterator.next();
                    iterator.remove();
                }
                //之后/否则执行新增记录
                loginRecordMap.put(uuid, expirationMill);
                String newJson = JsonKit.toJSON(oldAuthUser);
                entry.setValue(newJson);
                return entry;
            } finally {
                entry.getLock().unlock();
            }
        });
    }

    @Override
    protected void removeAuthUser(String key) {
        this.cache.remove(key);
    }

    @Override
    protected void updateAuthUser(String key, AuthUser authUser) {
        this.cache.compute(key, (k, entry) -> {
            if(entry == null) return null;
            entry.getLock().lock();
            try {
                MultipleAuthUser newAuthUser = (MultipleAuthUser) authUser;
                MultipleAuthUser oldAuthUser = JsonKit.toEntity((String) entry.getValue(), MultipleAuthUser.class);
                newAuthUser.copyAuthData(oldAuthUser);
                newAuthUser.copyCustomData(oldAuthUser);
                Object user = newAuthUser.getUser();
                oldAuthUser.setUser(user);
                String newJson = JsonKit.toJSON(oldAuthUser);
                entry.setValue(newJson);
                return entry;
            } finally {
                entry.getLock().unlock();
            }
        });
    }

    @Override
    protected void protected_loginOut(String key, String loginDeviceType, String uuid) {
        this.cache.compute(key, (k, entry) -> {
            if(entry == null) return null;
            entry.getLock().lock();
            try {
                MultipleAuthUser authUser = JsonKit.toEntity((String) entry.getValue(), MultipleAuthUser.class);
                ConcurrentHashMap<String, LinkedHashMap<String, Long>> keyAndDeviceAndLoginRecordMap =
                        authUser.getKeyAndDeviceAndLoginRecordMap();
                if(keyAndDeviceAndLoginRecordMap == null || keyAndDeviceAndLoginRecordMap.isEmpty()) {
                    return null;
                }
                Map<String, Long> loginRecordMap = keyAndDeviceAndLoginRecordMap.get(loginDeviceType);
                if(loginRecordMap == null || loginRecordMap.isEmpty()) {
                    keyAndDeviceAndLoginRecordMap.remove(loginDeviceType);
                    if(keyAndDeviceAndLoginRecordMap.isEmpty()) {
                        return null;
                    }
                    return entry;
                }
                loginRecordMap.remove(uuid);
                if(loginRecordMap.isEmpty()) {
                    keyAndDeviceAndLoginRecordMap.remove(loginDeviceType);
                    if(keyAndDeviceAndLoginRecordMap.isEmpty()) {
                        return null;
                    }
                }
                return entry;
            } finally {
                entry.getLock().unlock();
            }
        });
    }


    /* **************************************************************************************
     *
     *          私有逻辑
     *
     * **************************************************************************************
     * */

    @Note("定时轮询 - 清理过期的条目")
    private void private_timedPolling() {
        String name = this.protected_getInstanceName();
        Logger logger = this.protected_getLogger();
        if(this.cache.isEmpty()) {
            logger.info("MultipleAuthUser 缓存 {} 的定时轮询任务开始执行，检测到其内容为空，轮询任务结束!", name);
            return;
        }
        logger.info("MultipleAuthUser 缓存实例 {} 的定时轮询任务开始执行，准备批量移除已过期的登录用户...", name);
        StringBuilder expire = new StringBuilder();
        StringBuilder residual = new StringBuilder();
        Set<Map.Entry<String, RegularEntry>> entrySet = this.cache.entrySet();
        for (Map.Entry<String, RegularEntry> e : entrySet) {
            this.cache.compute(e.getKey(), (key, entry) -> {
                if(entry == null) return null;
                entry.getLock().lock();
                try {
                    MultipleAuthUser authUser = JsonKit.toEntity((String) entry.getValue(), MultipleAuthUser.class);
                    ConcurrentHashMap<String, LinkedHashMap<String, Long>> keyAndDeviceAndLoginRecordMap =
                            authUser.getKeyAndDeviceAndLoginRecordMap();
                    if(keyAndDeviceAndLoginRecordMap == null || keyAndDeviceAndLoginRecordMap.isEmpty()) {
                        this.private_deviceAndLoginRecordIsEmpty(expire, key, authUser);
                        return null;
                    }
                    for (Map.Entry<String, LinkedHashMap<String, Long>> e2 : keyAndDeviceAndLoginRecordMap.entrySet()) {
                        String loginDeviceType = e2.getKey();
                        LinkedHashMap<String, Long> loginRecordMap = e2.getValue();
                        if(loginRecordMap == null || loginRecordMap.isEmpty()) {
                            this.private_loginRecordMapIsEmpty(keyAndDeviceAndLoginRecordMap, expire,
                                    key, loginDeviceType, authUser);
                            return null;
                        }
                        for(Map.Entry<String, Long> e3 : loginRecordMap.entrySet()) {
                            String uuid = e3.getKey();
                            Long expiration = e3.getValue();
                            if(expiration != null && expiration >= System.currentTimeMillis()) {
                                loginRecordMap.remove(uuid);
                                if(loginRecordMap.isEmpty()) {
                                    this.private_loginRecordMapIsEmpty(keyAndDeviceAndLoginRecordMap, expire,
                                            key, loginDeviceType, authUser);
                                    return null;
                                }
                            } else {
                                residual.append('【').append(key).append("】->[")
                                        .append(loginDeviceType).append("]->[")
                                        .append(uuid).append("] 存在，过期时间是 ")
                                        .append(expiration).append(" 毫秒。对象: ")
                                        .append(authUser).append('\n');
                            }
                        }
                    }
                    return entry;
                } finally {
                    entry.getLock().unlock();
                }
            });
        }
        logger.info("MultipleAuthUser 缓存 {} 定时轮询任务结束，共移除以下条目: \n{}=================={}\n{}{}=================={}",
                this, BORDER, BORDER, expire, BORDER, BORDER
        );
        logger.info("MultipleAuthUser 缓存实例 {} 当前缓存剩余条目: \n{}=================={}\n{}{}=================={}",
                this, BORDER, BORDER, residual, BORDER, BORDER);
    }



    @Note("设备类型与登录记录 map 集合为空时做的日志输出操作")
    private void private_deviceAndLoginRecordIsEmpty(StringBuilder builder, String key, AuthUser authUser) {
        builder.append('【').append(key).append("】 的所有设备与登录记录为空。对象: ")
                .append(authUser).append('\n');
    }

    @Note("登录记录 map 集合为空时做的日志输出操作")
    private void private_loginRecordMapIsEmpty(ConcurrentHashMap<String, LinkedHashMap<String, Long>> keyAndDeviceAndLoginRecordMap,
                                               StringBuilder expire, String key,
                                               String loginDeviceType, AuthUser authUser) {
        expire.append('【').append(key).append("】-> ")
                .append(loginDeviceType).append("设备所有记录为空。对象: ")
                .append(authUser).append('\n');
        keyAndDeviceAndLoginRecordMap.remove(loginDeviceType);
        if(keyAndDeviceAndLoginRecordMap.isEmpty()) {
            this.private_deviceAndLoginRecordIsEmpty(expire, key, authUser);
        }
    }
}
